package co.recloud.ariadne.store;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;

import co.recloud.ariadne.model.Host;
import co.recloud.ariadne.model.Location;
import co.recloud.ariadne.thread.Main;

public class HostTable implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 8732746392097407621L;
	private Long localhostId;
	private SortedMap<Long, Host> tokenRing;
	private Set<Long> freeHosts;
	private Map<Long, Map<Long, Long>> locationToHost;
	private Map<Long, Integer> status;
	private Long width;
	private Long height;

	transient private static HostTable hostTable = null;

	public HostTable() {
		tokenRing = new ConcurrentSkipListMap<Long, Host>();
		freeHosts = new ConcurrentSkipListSet<Long>();
		locationToHost = new ConcurrentHashMap<Long, Map<Long, Long>>();
		status = new ConcurrentHashMap<Long, Integer>();
		width = 0L;
		height = 0L;
	}

	public static HostTable getInstance() {
		if (hostTable == null) {
			hostTable = new HostTable();
		}
		return hostTable;
	}

	public synchronized void addHost(Host host) {
		tokenRing.put(host.getToken(), host);
		status.put(host.getToken(), Main.STATUS_STANDBY);
	}

	public synchronized void addHosts(Map<Long, Host> hosts) {
		tokenRing.putAll(hosts);
	}

	/**
	 * @return the localhost
	 */
	public synchronized Host getLocalhost() {
		return tokenRing.get(localhostId);
	}

	public synchronized Host findByHash(Long hash) {
		Host result = null;
		SortedMap<Long, Host> tailMap = tokenRing.tailMap(hash);
		Long currentKey = null;
		if (tailMap != null && tailMap.size() > 0) {
			currentKey = tailMap.firstKey();
		} else {
			currentKey = tokenRing.firstKey();
		}
		for(int i = 0; i < tokenRing.size() && tokenRing.get(currentKey).getLocation() == null; i++) {
			if(!tailMap.isEmpty()) {
				currentKey = tokenRing.tailMap(currentKey).firstKey();
			} else {
				currentKey = tokenRing.firstKey();
			}
		}
		result = tokenRing.get(currentKey);
		return result;
	}

	public synchronized boolean isReplica(Location primaryLocation) {
		Location myLocation = getLocalhost().getLocation();
		if (myLocation != null && primaryLocation != null) {
			if (0 == (myLocation.getX() - primaryLocation.getX() - 1) % width
					&& 0 == (myLocation.getY() - primaryLocation.getY())
							% height) {
				return true;
			}
			if (0 == (myLocation.getX() - primaryLocation.getX()) % width
					&& 0 == (myLocation.getY() - primaryLocation.getY() - 1)
							% height) {
				return true;
			}
			if (myLocation.getX().equals(primaryLocation.getX())
					&& myLocation.getY().equals(primaryLocation.getY())) {
				return true;
			}
		}
		return false;
	}

	public synchronized Host getCoReplica(Host primaryHost, Host host) {
		Location primaryLocation = primaryHost.getLocation();
		Location startLocation = host.getLocation();
		Long x = null;
		Long y = null;
		Host result = null;
		if (0 == (startLocation.getX() - primaryLocation.getX() - 1) % width
				&& 0 == (startLocation.getY() - primaryLocation.getY())
						% height) {
			x = (startLocation.getX() - 1) % width;
			y = (startLocation.getY() + 1) % height;
		} else if (0 == (startLocation.getX() - primaryLocation.getX()) % width
				&& 0 == (startLocation.getY() - primaryLocation.getY() - 1)
						% height) {
			x = startLocation.getX();
			y = (startLocation.getY() - 1) % height;
		} else if (startLocation.getX().equals(primaryLocation.getX())
				&& startLocation.getY().equals(primaryLocation.getY())) {
			x = (startLocation.getX() + 1) % width;
			y = startLocation.getY();
		}
		if(x != null && y != null) {
			if(x < 0) {
				x += width;
			}
			if(y < 0) {
				y += height;
			}
			if(locationToHost.get(x) != null) {
				if(locationToHost.get(x).get(y) != null) {
					result = tokenRing.get(locationToHost.get(x).get(y));
				}
			}
		}
		return result;
	}

	public synchronized Set<Host> getColumn() {
		// TODO Auto-generated method stub
		return null;
	}

	public synchronized Set<Host> getRow() {
		return null;
	}

	public synchronized boolean expandGrid() {
		long max = Math.max(width, height) + 1;
		int o;
		if (max == width + 1) {
			o = 0;
		} else {
			o = 1;
		}
		System.out.println("free hosts: " + freeHosts);
		if (freeHosts.size() > 1 && freeHosts.size() >= max - 1 && max > 1) {
			Iterator<Long> hostIt = freeHosts.iterator();
			Set<Host> removeSet = new HashSet<Host>();
			for (long i = 0; i < max - 1; i++) {
				Host host = tokenRing.get(hostIt.next());
				removeSet.add(host);
				Location newLocation = new Location();
				if (o == 1) {
					newLocation.setX(width);
					newLocation.setY(i);
				} else {
					newLocation.setX(i);
					newLocation.setY(height);
				}
				mapLocationToHost(newLocation, host);
			}
			updateFreeHosts();
			if (o == 0) {
				height++;
			} else {
				width++;
			}
			System.out.println(locationToHost);
			return true;
		} else if (max == 1) {
			System.out.println("setting up first hosts");
			height = 1L;
			for (Long token : freeHosts) {
				Host host = tokenRing.get(token);
				Location newLocation = new Location();
				newLocation.setX(width);
				newLocation.setY(0L);
				mapLocationToHost(newLocation, host);
				width++;
			}
			freeHosts.clear();
			System.out.println(locationToHost);
			return true;
		}
		return false;
	}
	
	public void mapLocationToHost(Location newLocation, Host host) {
		host.setLocation(newLocation);
		if(locationToHost.get(newLocation.getX()) == null) {
			locationToHost.put(newLocation.getX(), new ConcurrentHashMap<Long, Long>());
		}
		locationToHost.get(newLocation.getX()).put(newLocation.getY(), host.getToken());
	}

	public Map<Long, Map<Long, Long>> getLocationToHost() {
		return locationToHost;
	}

	public void merge(HostTable hostTable2) {
		for(Long token : hostTable2.tokenRing.keySet()) {
			Host host = hostTable2.tokenRing.get(token);
			if(host != null) {
				Host myHost = tokenRing.get(token);
				if(host.getToken() != null && myHost == null) {
					tokenRing.put(host.getToken(), host);
					myHost = host;
				}
				if(host.equals(myHost)) {
					if(host.getLocation() != null) {
						myHost.setLocation(host.getLocation());
						mapLocationToHost(host.getLocation(), myHost);
					}
				}
			}
		}
		updateFreeHosts();
		if(hostTable2.width > width) {
			width = hostTable2.width;
		} 
		if(hostTable2.height > height) {
			height = hostTable2.height;
		}
	}

	public void updateFreeHosts() {
		freeHosts.clear();
		freeHosts.addAll(tokenRing.keySet());
		for(Map<Long, Long> mappedLocations : locationToHost.values()) {
			freeHosts.removeAll(mappedLocations.values());
		}
	}
	
	public void setLocalhost(String localHostName, int port) {
		Host localhost = new Host();
		localhost.setHostName(localHostName);
		localhost.setPort(port);
		localhost.generateToken();
		localhostId = localhost.getToken();
		addHost(localhost);
		addFreeHost(localhost.getToken());
	}

	public Map<Long, Host> getTokenRing() {
		return tokenRing;
	}

	public void addFreeHost(Long token) {
		freeHosts.add(token);
	}

	public void setWidth(Long width) {
		this.width = width;
	}
	
	public void setHeight(Long height) {
		this.height = height;
	}

	public Long getWidth() {
		return width;
	}
	
	public Long getHeight() {
		return height;
	}
	
	public Set<Host> getHostsInService() {
		Set<Host> hosts = new HashSet<Host>();
		for(Long x : locationToHost.keySet()) {
			for(Long y : locationToHost.get(x).keySet()) {
				hosts.add(tokenRing.get(locationToHost.get(x).get(y)));
			}
		}
		return hosts;
	}

	public Host getHostAtLocation(Location location) {
		Host result = null;
		if(locationToHost.get(location.getX()) != null) {
			if(locationToHost.get(location.getX()).get(location.getY()) != null) {
				result = tokenRing.get(locationToHost.get(location.getX()).get(location.getY()));
			}
		}
		return result;
	}

	public Set<Long> getFreeHosts() {
		return freeHosts;
	}

	public Set<Host> getRandomNeighbors() {
		Set<Host> neighbors = new HashSet<Host>();
		List<Host> hosts = new LinkedList<Host>();
		hosts.addAll(tokenRing.values());
		for(int i = 0; i < hosts.size(); i++) {
			if(!hosts.get(i).getToken().equals(localhostId)) {
				double test = Math.random();
				if(test > .4) {
					neighbors.add(hosts.get(i));
				}
			}
		}
		return neighbors;
	}
	
	public Host getBestCoHost(Host host) {
		Host coHost = null;
		if (getLocalhost().getLocation().equals(host.getLocation())) {
			coHost = getCoReplica(host, getLocalhost());
		} else if (isReplica(host.getLocation())) {
			coHost = getLocalhost();
		} else {
			coHost = getCoReplica(host, host);
		}
		return coHost;
	}

	public Host getRecoveryReplica() {
		Host replica = null;
		Location location = getLocalhost().getLocation();
		Long x = (location.getX() - 1) % width;
		Long y = location.getY();
		if(x < 0) {
			x += width;
		}
		replica = tokenRing.get(locationToHost.get(x).get(y));
		return replica;
	}

	public void recoverGrid(Host replica) {
		if(freeHosts.isEmpty()) {		
			
		}
	}
}
